package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.*;
import org.apache.tomcat.util.buf.StringUtils;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisCommontCreateUtils.*;
import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.RedisConstants.SHOP_CACHE_KEY;
import static com.hmdp.utils.RedisLockUtil.tryLock;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedisIdGenerate redisIdGenerate;


    /**
     * 根据id查询店铺信息
     *
     * @param id
     * @return
     */
    @Override
    public Result queryShopById(Long id) {
        //Result result = queryShopByIdWithMutexLock(id);//使用互斥锁来解决缓存击穿
        //Result result1 = queryShopByIdWithLogicEx(id); //使用逻辑过期来解决缓存击穿
        //Shop shop = queryWithLogicalExpire(SHOP_CACHE_KEY, id, stringRedisTemplate, Shop.class, this::getById, Duration.ofMinutes(60));
        Shop shop = queryWithMuteLock(SHOP_CACHE_KEY, id, stringRedisTemplate, Shop.class, this::getById, Duration.ofMinutes(60));
        testGenerateId();

        return Result.ok(shop);//返回店铺信息
    }

    private void testGenerateId() {
        try {
            /*测试全局id生成器*/
            long generateId = redisIdGenerate.generateId(SHOP_CACHE_KEY);
        } catch (Exception e) {
            log.error("生成id失败", e);
        }
    }

    public Result queryShopByIdWithMutexLock(Long id) {
        String key = SHOP_CACHE_KEY + id; // cache:shop:id
        String lockKey = "lock:queryShopById";
        boolean tryLock = false;


        while (!tryLock) {
            // 1. 从Redis中查询店铺信息
            String shopStr = (String) redisTemplate.opsForValue().get(key);

            // 2. 判断是否存在
            if (StrUtil.isNotBlank(shopStr)) { // 如果Redis中存在
                Shop shop = JSONUtil.toBean(shopStr, Shop.class);
                return Result.ok(shop); // 返回店铺信息
            }

            // 3. 如果为空对象（空值），则返回错误信息
            if (shopStr != null) {
                log.error("此商铺数据为空,缓存中的值为空值");
                return Result.fail("此店铺不存在");
            }

            // 4. 尝试获取锁
            tryLock = tryLock(lockKey, stringRedisTemplate);
            if (!tryLock) {
                try {
                    Thread.sleep(100); // 获取锁失败，休眠100ms
                } catch (InterruptedException e) {
                    log.error("休眠异常", e);
                    Thread.currentThread().interrupt(); // 恢复中断状态
                    return Result.fail("系统繁忙，请稍后重试");
                }
            }
        }

        Shop shop = null;
        try {
            // 5. 如果为null，则查询数据库
            shop = getById(id);
            if (Objects.isNull(shop)) { // 如果数据库中不存在
                // 缓存空对象到Redis中，防止缓存穿透
                redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(new Shop()), 5, TimeUnit.MINUTES);
                log.error("此店铺不存在");
                return Result.fail("此店铺不存在");
            }

            // 6. 存在，将店铺信息存入Redis中
            redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RANDOM_NUMBER.nextInt(60) + 1, TimeUnit.MINUTES);
            log.debug("缓存店铺信息成功");
        } catch (Exception e) {
            log.error("缓存店铺信息失败", e);
            // 缓存失败不影响主流程，继续返回店铺信息
        } finally {
            // 释放锁
            RedisLockUtil.unLock(lockKey, stringRedisTemplate);
        }

        // 7. 返回店铺信息
        return Result.ok(shop);
    }


    /**
     * 根据id查询店铺信息，并缓存到Redis中，并设置逻辑过期时间<br>
     * 通过逻辑过期的方案来解决缓存击穿的问题<br>
     *
     * @param id
     * @return
     */
    public Result queryShopByIdWithLogicEx(Long id) {
        String cacheKey = SHOP_CACHE_KEY + id; // 缓存键：cache:shop:id
        String lockKey = "lock:queryShopById"; // 互斥锁

        // 1. 从Redis中查询店铺信息
        String cachedShopStr = (String) redisTemplate.opsForValue().get(cacheKey);
        if (StrUtil.isBlank(cachedShopStr)) {
            log.error("此店铺不存在");
            return null; // 缓存中不存在，直接返回null
        }

        // 2. 解析缓存数据
        RedisData redisData = JSONUtil.toBean(cachedShopStr, RedisData.class);
        if (redisData == null || redisData.getData() == null) {
            log.error("缓存数据解析失败");
            return null;
        }

        // 3. 提取店铺信息
        JSONObject shopJson = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);

        // 4. 判断缓存是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if (LocalDateTime.now().isBefore(expireTime)) {
            return Result.ok(shop); // 缓存未过期，直接返回
        }

        // 5. 缓存过期，尝试获取锁
        boolean lockAcquired = tryLock(lockKey, stringRedisTemplate);
        if (!lockAcquired) {
            return Result.ok(shop); // 获取锁失败，返回当前缓存数据
        }

        // 6. 获取锁成功，异步重建缓存
        new Thread(() -> {
            try {
                // 查询数据库
                Shop dbShop = getById(id);
                if (dbShop == null) {
                    // 缓存空对象，防止缓存穿透
                    redisTemplate.opsForValue().set(cacheKey, JSONUtil.toJsonStr(new Shop()), 5, TimeUnit.MINUTES);
                    log.error("此店铺不存在");
                } else {
                    // 更新缓存
                    //saveShopToRedisWithExpire(dbShop, cacheKey, Duration.ofDays(10));
                    setJsonValueWithExpire2(cacheKey, dbShop, Duration.ofDays(10), stringRedisTemplate);
                    log.debug("缓存店铺信息成功");
                }
            } catch (Exception e) {
                log.error("缓存重建失败", e);
            } finally {
                // 确保锁被释放
                RedisLockUtil.unLock(lockKey, stringRedisTemplate);
            }
        }).start();

        // 7. 返回当前缓存数据
        return Result.ok(shop);
    }


    /**
     * 将店铺信息保存到Redis中，并设置逻辑过期时间
     */
    private void saveShopToRedisWithExpire(Shop shop, String key, Duration expireDuration) {
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plus(expireDuration));/*设置过期时间为现在开始的10天*/
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }


    @Override
    @Transactional//事务注解,作用是将方法的多个操作全部成功或全部失败,不能只操作数据库不操作缓存
    public Result updateShopById(Shop shop) {
        //业务健壮性判断
        if (shop == null || shop.getId() == null) {
            return Result.fail("店铺信息不能为空");
        }
        //业务:根据id更新店铺信息
        boolean b = updateById(shop);
        if (!b) {
            return Result.fail("更新失败");
        }
        //删除缓存
        String key = SHOP_CACHE_KEY + shop.getId();
        //根据key删除缓存
        redisTemplate.delete(key);//根据key删除缓存
        //删除以key开头的缓存
        String format = StrUtil.format(SHOP_CACHE_KEY + "*");
        System.out.println("format:" + format);
        //返回成功信息
        return Result.ok();//返回成功信息
    }

    /**
     * 根据类型查询店铺信息,看情况是否需要根据地理位置查询,前端有传递x,y坐标,则根据地理位置查询,否则根据类型查询
     *
     * @param typeId
     * @param current
     * @param x
     * @param y
     * @return
     */
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //0. 判断是否根据地理位置查询
        if (x == null || y == null) {
            // 根据类型分页查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }
        //1. 计算开始页码和结束页码,在redis中查询缓存
        int start = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;//当前页码减1乘以每页显示的条数
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;//当前页码乘以每页显示的条数.此时代表下一页的开始位置

        //2. 根据类型分页查询缓存
        String key = SHOP_GEO_KEY + typeId;
        // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoLocationGeoResults = stringRedisTemplate.opsForGeo()
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),//半径5000米
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance()//返回距离
                                .limit(end)//从第0个开始读取,读取count个元素,从头读到end位置,最多读取count个元素 count为3 读取:0,1,2
                );
        //3. 手动截取start到end的分页数据
        if (geoLocationGeoResults == null || geoLocationGeoResults.getContent().isEmpty()) {
            //为空代表附近没有店铺,返回空集合
            log.debug("附近没有店铺");
            return Result.ok(Collections.emptyList());

        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = geoLocationGeoResults.getContent();//包含坐标点和位置名的GeoLocation对象集合
        //redis中店铺坐标条数小于或者等于开始页码:代表数据在上一页就读完了,没有下一页了
        if (content.size() <= start) {
            log.debug("数据在上一页就读完了,没有下一页了");
            return Result.ok(Collections.emptyList());
        }
        //数据截取 从start开始到end结束(>=start,<end)
        List<Long> storeIdList = new ArrayList<>(content.size());//店铺id集合
        //到某个店铺的距离店铺id和距离的map
        Map<String, Distance> distanceMap = new HashMap<>(content.size());//店铺id和距离的map
        content.stream().skip(start).forEach(
                (geoLocationGeoResult) -> {
                    //获取店铺id
                    String storeIdStr = geoLocationGeoResult.getContent().getName();
                    Long.valueOf(storeIdStr);
                    storeIdList.add(Long.valueOf(storeIdStr));
                    //获取用户到店铺的距离
                    Distance distance = geoLocationGeoResult.getDistance();
                    distanceMap.put(storeIdStr, distance);
                }
        );
        //4.根据店铺id集合查询店铺信息
        String storeIdListStr = StrUtil.join(",", storeIdList);
        List<Shop> shopList = query().in("id", storeIdList).last("ORDER BY FIELD(id," + storeIdListStr + ")").list();
        //设置距离属性
        for (Shop shop : shopList) {
            //设置距离属性
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        //5.返回店铺信息
        return Result.ok(shopList);
    }


//    @Override
//    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
//        // 1.判断是否需要根据坐标查询
//        if (x == null || y == null) {
//            // 不需要坐标查询，按数据库查询
//            Page<Shop> page = query()
//                    .eq("type_id", typeId)
//                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
//            // 返回数据
//            return Result.ok(page.getRecords());
//        }
//
//        // 2.计算分页参数
//        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
//        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
//
//        // 3.查询redis、按照距离排序、分页。结果：shopId、distance
//        String key = SHOP_GEO_KEY + typeId;
//        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
//                .search(
//                        key,
//                        GeoReference.fromCoordinate(x, y),
//                        new Distance(5000),
//                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
//                );
//        // 4.解析出id
//        if (results == null) {
//            return Result.ok(Collections.emptyList());
//        }
//        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
//        if (list.size() <= from) {
//            // 没有下一页了，结束
//            return Result.ok(Collections.emptyList());
//        }
//        // 4.1.截取 from ~ end的部分
//        List<Long> ids = new ArrayList<>(list.size());
//        Map<String, Distance> distanceMap = new HashMap<>(list.size());
//        list.stream().skip(from).forEach(result -> {
//            // 4.2.获取店铺id
//            String shopIdStr = result.getContent().getName();
//            ids.add(Long.valueOf(shopIdStr));
//            // 4.3.获取距离
//            Distance distance = result.getDistance();
//            distanceMap.put(shopIdStr, distance);
//        });
//        // 5.根据id查询Shop
//        String idStr = StrUtil.join(",", ids);
//        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
//        for (Shop shop : shops) {
//            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
//        }
//        // 6.返回
//        return Result.ok(shops);
//    }
}
